﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Affinity.Synthesis.ExpressionCells.Metadata;

namespace Affinity.Synthesis.ExpressionCells.Basis
{
    /// <summary>
    /// Represents a LINQ query method.
    /// Tracks elements of the input collection for relevant property change events.
    /// </summary>
    internal class QueryMethodInvocationCell : MethodInvocationCell
    {
        /// <summary>
        /// Constructor accepting the MethodCallExpression corresponding to a LINQ query and a delegate representing the method provided to it.
        /// </summary>
        /// <param name="methodCallExpression"></param>
        /// <param name="methodDelegate"></param>
        public QueryMethodInvocationCell(MethodCallExpression methodCallExpression, Delegate methodDelegate)
            : base(methodCallExpression, methodDelegate)
        {
            _QueryElementType = methodCallExpression.Method.GetGenericArguments().Last();
            _ToArrayMethod = typeof(Enumerable).GetMethod("ToArray").MakeGenericMethod(_QueryElementType);
            _ShouldConvertOutputToArray = methodCallExpression.Method.ReturnType.IsGenericType && methodCallExpression.Method.ReturnType.GetGenericTypeDefinition() == typeof(IEnumerable<>);
        }

        /// <summary>
        /// Copy constructor.
        /// </summary>
        /// <param name="existing"></param>
        protected QueryMethodInvocationCell(QueryMethodInvocationCell existing)
            : base(existing)
        {
            _QueryElementType = existing._QueryElementType;
            _ToArrayMethod = existing._ToArrayMethod;
            _ShouldConvertOutputToArray = existing._ShouldConvertOutputToArray;
        }

        /// <summary>
        /// Creates and returns a copy of this expression cell.
        /// </summary>
        /// <returns></returns>
        public override IExpressionCell Copy()
        {
            return new QueryMethodInvocationCell(this);
        }

        /// <summary>
        /// "Calculates" the value of this expression cell by invoking the LINQ query on the inputs.
        /// </summary>
        /// <param name="inputs"></param>
        /// <param name="hasValue"></param>
        /// <param name="value"></param>
        protected override void CalculateOutput(object[] inputs, out bool hasValue, out object value)
        {
            if (_MonitoredPropertyNames.Count > 0)
            {
                SetTrackedItems(inputs.OfType<IEnumerable>().SelectMany(
                    enumerable => enumerable.OfType<INotifyPropertyChanged>()).ToArray());
            }
            else
            {
                SetTrackedItems(new INotifyPropertyChanged[0]);
            }

            base.CalculateOutput(inputs, out hasValue, out value);

            if (_ShouldConvertOutputToArray && hasValue && value != null)
            {
                value = _ToArrayMethod.Invoke(null, new[] { value });
            }
        }

        /// <summary>
        /// Called when one or more of the inputs have changed.
        /// </summary>
        protected override void OnInputsChanged()
        {
            _MonitoredPropertyNames.Clear();

            foreach (PropertyDependency propertyDependency in Inputs[1].OwningExpressionCell.Metadata.OfType<PropertyDependency>())
            {
                if (propertyDependency.Property.DeclaringType != null && propertyDependency.Property.DeclaringType.IsAssignableFrom(_QueryElementType))
                {
                    _MonitoredPropertyNames.Add(propertyDependency.Property.Name);
                }
            }

            base.OnInputsChanged();
        }

        #region Item Tracking

        /// <summary>
        /// Begins tracking property changes on the specified items.
        /// </summary>
        /// <param name="items"></param>
        private void AddTrackedItems(IEnumerable<INotifyPropertyChanged> items)
        {
            foreach (var item in items)
            {
                _WeakPropertyChangedCallback.Add(item);
                _TrackedItems.Add(item);
            }
        }

        /// <summary>
        /// Handles relevant property change events on elements in the input collection.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (_MonitoredPropertyNames.Contains(e.PropertyName))
            {
                //Debug.WriteLine("QueryMethodInvocationCell: Property {0} changed", e.PropertyName);
                OnValueDirty();
            }
        }

        /// <summary>
        /// Ceases tracking of property changes on the specified items.
        /// </summary>
        /// <param name="items"></param>
        private void RemoveTrackedItems(IEnumerable<INotifyPropertyChanged> items)
        {
            foreach (var item in items)
            {
                _WeakPropertyChangedCallback.Remove(item);
                _TrackedItems.Remove(item);
            }
        }

        /// <summary>
        /// Sets the specified items as the ones that should be monitored for property change events.
        /// </summary>
        /// <param name="items"></param>
        protected void SetTrackedItems(INotifyPropertyChanged[] items)
        {
            if (_WeakPropertyChangedCallback == null) _WeakPropertyChangedCallback = new WeakPropertyChangedCallback(this);

            INotifyPropertyChanged[] removedItems = _TrackedItems.Except(items).ToArray();
            INotifyPropertyChanged[] addedItems = items.Except(_TrackedItems).ToArray();

            RemoveTrackedItems(removedItems);
            AddTrackedItems(addedItems);
        }

        #endregion


        private HashSet<string> _MonitoredPropertyNames = new HashSet<string>();
        private Type _QueryElementType = null;
        private bool _ShouldConvertOutputToArray = false;
        private MethodInfo _ToArrayMethod = null;
        private List<INotifyPropertyChanged> _TrackedItems = new List<INotifyPropertyChanged>();
        private WeakPropertyChangedCallback _WeakPropertyChangedCallback = null;

        /// <summary>
        /// Provides weak event handling for INotifyPropertyChanged.PropertyChanged events.
        /// </summary>
        private class WeakPropertyChangedCallback
        {
            /// <summary>
            /// Constructor accepting a reference to the owning QueryMethodInvocationCell.
            /// </summary>
            /// <param name="cell"></param>
            public WeakPropertyChangedCallback(QueryMethodInvocationCell cell)
            {
                _WeakCellReference = new WeakReference(cell);
            }

            /// <summary>
            /// Adds a listener to the specified INotifyPropertyChanged object's PropertyChanged event.
            /// </summary>
            /// <param name="notifyPropertyChanged"></param>
            public void Add(INotifyPropertyChanged notifyPropertyChanged)
            {
                notifyPropertyChanged.PropertyChanged += HandlePropertyChanged;
            }

            /// <summary>
            /// Removes a listener from the specified INotifyPropertyChanged object's PropertyChanged event.
            /// </summary>
            /// <param name="notifyPropertyChanged"></param>
            public void Remove(INotifyPropertyChanged notifyPropertyChanged)
            {
                notifyPropertyChanged.PropertyChanged -= HandlePropertyChanged;
            }

            /// <summary>
            /// Handles an INotifyPropertyChanged.PropertyChanged event.
            /// If the owning QueryMethodInvocationCell has been garbage collected, this method removes itself from PropertyChanged.
            /// Otherwise, it calls HandlePropertyChanged on the owning QueryMethodInvocationCell.
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                QueryMethodInvocationCell strongCellReference = (QueryMethodInvocationCell)_WeakCellReference.Target;
                if (strongCellReference == null) Remove((INotifyPropertyChanged)sender);
                else strongCellReference.HandlePropertyChanged(sender, e);
            }

            private readonly WeakReference _WeakCellReference = null;
        }
    }
}
